#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2011 BidiX (http://bidix.info)
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
'''
gae_client
**********
	HTTP/HTTPS Client to Google App Engine.
	
	Description :
		- Issue request to a GAE app
		- Login to service if it is requested
		- Manage authentication token in a cookie file 
	
 
	Usage:
		client = GAE_Client(email, password, domain, gae_app, cookies_filename, local=False, scheme='HTTP')
		 
		(response_code, response_body) = client.request(method, url, data=None)  
	 
'''
import sys
import cookielib
from urllib import urlencode
import urllib2
import re




class RequestWithMethod(urllib2.Request):
	"""
	Hack for forcing the method in a request â€“ allows POST, PUT and DELETE
	
	http://benjamin.smedbergs.us/blog/2008-10-21/putting-and-deleteing-in-python-urllib2/
	"""
	def __init__(self, method, *args, **kwargs):
		self._method = method
		urllib2.Request.__init__(self, *args, **kwargs)

	def get_method(self):
		if self._method:
			return self._method
		elif self.has_data():
			return "POST"
		else:
			return "GET"


	
class GAE_Client():
	"""
	HTTP/HTTPS Client to a Google App Engine Application.
	
	GAE_Client encapsulates request to a Google App Engine App (*gae_app*). When requested, issues 
	a login on a Google App Domain or on Google Accounts using *email*, *password* and *domain*.
		
	*local* and *local_admin* are used to login to a GAE development server.
	
	*scheme* (HTTP or HTTPS) is the default scheme to access to *gae_app*, when no scheme is specified 
	in the request. This *scheme* is used to login.
	"""
	# login resources
	token_host = 'www.google.com'
	token_url = 'https://www.google.com/accounts/ClientLogin'
	login_url = '%s://%s.appspot.com/_ah/login?%s' # using scheme, gae_app, auth_data
	# login page detection  
	_remote_login_regex = re.compile('id=\"gaia_')
	_local_login_regex = re.compile('<form method=\"get\" action=\"/_ah/login\"')
	# gae app host in url
	local_host = 'http://localhost:8080'
	remote_host = '%s://%s.appspot.com'

	def __init__(self, email, password, domain, gae_app, cookies_filename, local=False, local_admin=True, scheme='http'):
		"""
		Install an HTTPCookieProcessor 
		Try to load the cookies_filename
		
		The scheme is the default scheme used for the app. This scheme is used for login.
		"""
		self.email = email
		self.password = password
		self.source = domain
		self.gae_app = gae_app
		self.cookiejar = cookielib.LWPCookieJar()
		self.cookiejar.filename = cookies_filename
		self.local = local
		self.local_admin = local_admin
		self.scheme = scheme
		
		if self.local:
			self.request_host = self.local_host
			self.login_regex = self._local_login_regex
		else:
			self.request_host = self.remote_host%(self.scheme, self.gae_app)
			self.login_regex = self._remote_login_regex
		try:
			self.cookiejar.load()
		except:
			pass 
		self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookiejar))
		urllib2.install_opener(self.opener)
		
	def __del__(self):
		"""
		Save cookiejar file on destruction
		"""
		self.cookiejar.save()

	def _login(self, target_url):
		"""
		Issue a login using constructor parameters.
		
		If local, use local_host and dev_server
		"""
		if not self.local:
			authreq_data = urlencode({'Email': self.email,
										 'Passwd': self.password,
										 'service': 'ah',
										 'source': self.source,
										 'accountType': 'HOSTED_OR_GOOGLE'})
			auth_req = urllib2.Request(self.token_url, data=authreq_data)
			auth_resp = self.opener.open(auth_req)
			auth_resp_body = auth_resp.read()
			auth_resp_dict = dict(x.split('=')
								 for x in auth_resp_body.split('\n') if x)
			authtoken = auth_resp_dict['Auth']
			authreq_data = urlencode({
										 'continue': target_url,
										 'auth': authtoken})
			login_url = self.login_url % (self.scheme, self.gae_app, authreq_data)
		else:
			authreq_data = urlencode({'email': self.email,
										 'continue': target_url,
										 'admin': self.local_admin,
										 'action': 'Login'})
			login_url = ('%s/_ah/login?%s' % (self.local_host, authreq_data))

		serv_req = urllib2.Request(login_url)
		response = self.opener.open(serv_req)
		self.cookiejar.save()
		return response
	
	def request(self, method, url, data=None, scheme=None):
		"""
		Issues a request to gae app.
		
		Args:
			- method: GET, POST, PUT, DELETE.  
			- url: only path and query string.  
			- data: body of the request for a PUT or POST method.  
			- scheme: HTTP or HTTPS. Override the default client scheme.  
			
		Returns:
			(response_code, response_body)
		"""
		if scheme:
			request_host = self.remote_host%(self.scheme, self.gae_app)
		else:
			request_host = self.request_host
		full_url = request_host + url
		req = RequestWithMethod(method, full_url, data)
		response = None
		try:
			response = self.opener.open(req, data)
			response_body = response.read()
		except urllib2.HTTPError, error:
			if error.code != 302 and error.code != 401:
				raise urllib2.HTTPError(full_url, error.code, error.msg, None, None)
		if not response or self.login_regex.search(response_body):
			# it's a login page
			try:
				if method == 'POST':
					response = self._login('/')
					response_code, response_body = self.request(method, url, data)
					return (response_code, response_body)
				else:
					response = self._login(full_url)
					response_body = response.read()
					return (response.code, response_body)
			except urllib2.HTTPError, error:
				if error.code == 403:
					return (401, "unauthorized - Login failed")
				else:
					return (error.code, error.msg)
		else:
			return (response.code, response_body)